/*
 * Encog(tm) Core v3.4 - Java Version
 * http://www.heatonresearch.com/encog/
 * https://github.com/encog/encog-java-core
 
 * Copyright 2008-2017 Heaton Research, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *   
 * For more information on Heaton Research copyrights, licenses 
 * and trademarks visit:
 * http://www.heatonresearch.com/copyright
 */
package org.encog.ml.bayesian.bif;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.encog.Encog;
import org.encog.ml.bayesian.BayesianChoice;
import org.encog.ml.bayesian.BayesianError;
import org.encog.ml.bayesian.BayesianEvent;
import org.encog.ml.bayesian.BayesianNetwork;
import org.encog.ml.bayesian.table.TableLine;
import org.encog.parse.tags.write.WriteXML;
import org.encog.util.csv.CSVFormat;
import org.xml.sax.SAXException;

/**
 * A utility class to read and write Bayesian networks in BIF format.
 * 
 * http://www.heatonresearch.com/wiki/Bayesian_Interchange_Format
 */
public class BIFUtil {

	/**
	 * Read a BIF file.
	 * 
	 * @param f
	 *            The BIF file.
	 * @return The Bayesian network that was read.
	 */
	public static BayesianNetwork readBIF(String f) {
		return readBIF(new File(f));
	}

	public static BayesianNetwork readBIF(File f) {
		FileInputStream fis = null;

		try {
			fis = new FileInputStream(f);
			return readBIF(fis);
		} catch (IOException ex) {
			throw new BayesianError(ex);
		} finally {
			if (fis != null) {
				try {
					fis.close();
				} catch (IOException ex) {
					// who cares at this point.
				}
			}
		}
	}

	/**
	 * Read a BIF file from a stream.
	 * 
	 * @param is
	 *            The stream to read from.
	 * @return The Bayesian network read.
	 */
	public static BayesianNetwork readBIF(InputStream is) {
		try {
			BIFHandler h = new BIFHandler();
			SAXParserFactory spf = SAXParserFactory.newInstance();
			SAXParser sp = spf.newSAXParser();
			sp.parse(is, h);
			return h.getNetwork();
		} catch (IOException ex) {
			throw new BayesianError(ex);
		} catch (ParserConfigurationException ex) {
			throw new BayesianError(ex);
		} catch (SAXException ex) {
			throw new BayesianError(ex);
		}
	}

	/**
	 * Write a Bayesian network to BIF form.
	 * 
	 * @param fn
	 *            The file name to save to.
	 * @param network
	 *            The network to save.
	 */
	public static void writeBIF(String fn, BayesianNetwork network) {
		writeBIF(new File(fn), network);
	}

	/**
	 * Write a Bayesian network to a BIF file.
	 * 
	 * @param file
	 *            The file to save to.
	 * @param network
	 *            The network to save.
	 */
	public static void writeBIF(File file, BayesianNetwork network) {
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(file);
			writeBIF(fos, network);
		} catch (IOException ex) {
			throw new BayesianError(ex);
		} finally {
			if (fos != null) {
				try {
					fos.close();
				} catch (IOException e) {
					// don't care at this point
				}
			}
		}
	}

	/**
	 * Write a Bayesian network to an output stream in BIF format.
	 * 
	 * @param os
	 *            The output stream to write to.
	 * @param network
	 *            The network to write.
	 */
	public static void writeBIF(OutputStream os, BayesianNetwork network) {
		WriteXML xml = new WriteXML(os);
		xml.beginDocument();
		xml.addAttribute("VERSION", "0.3");
		xml.beginTag("BIF");
		xml.beginTag("NETWORK");
		xml.addProperty("NAME", "Bayes Network, Generated by Encog");
		// write variables
		for (BayesianEvent event : network.getEvents()) {
			xml.addAttribute("TYPE", "nature");
			xml.beginTag("VARIABLE");
			xml.addProperty("NAME", event.getLabel());
			for (BayesianChoice str : event.getChoices()) {
				xml.addProperty("OUTCOME", str.getLabel());
			}
			xml.endTag();
		}

		// write relations
		for (BayesianEvent event : network.getEvents()) {
			xml.beginTag("DEFINITION");
			xml.addProperty("FOR", event.getLabel());
			for (BayesianEvent parentEvent : event.getParents()) {
				xml.addProperty("GIVEN", parentEvent.getLabel());
			}
			xml.addAttribute("TABLE", generateTable(event));
			xml.endTag();
		}

		xml.endTag();
		xml.endTag();
		xml.endDocument();

	}

	/**
	 * Generate a table, in BIF format.
	 * 
	 * @param event
	 *            The event to write.
	 * @return The string form of the table.
	 */
	public static String generateTable(BayesianEvent event) {
		StringBuilder s = new StringBuilder();
		int tableIndex = 0;
		int[] args = new int[event.getParents().size()];
		do {
			for (int result = 0; result < event.getChoices().size(); result++) {
				TableLine line = event.getTable().findLine(result, args);
				if (s.length() > 0) {
					s.append(" ");
				}
				s.append(CSVFormat.EG_FORMAT.format(line.getProbability(),
						Encog.DEFAULT_PRECISION));
			}
		} while (BIFUtil.rollArgs(event, args));
		return s.toString();
	}

	/**
	 * Iterate through the event arguments in the BIF way, which is different
	 * than Encog's method.
	 * 
	 * @param event
	 *            The event to save.
	 * @param args
	 *            The arguments.
	 * @return True if there is further to iterate.
	 */
	public static boolean rollArgs(BayesianEvent event, int[] args) {
		int currentIndex = event.getParents().size() - 1;
		boolean done = false;
		boolean eof = false;

		if (event.getParents().size() == 0) {
			done = true;
			eof = true;
		}

		while (!done) {
			int v = (int) args[currentIndex];
			v++;
			if (v >= event.getParents().get(currentIndex).getChoices().size()) {
				args[currentIndex] = 0;
			} else {
				args[currentIndex] = v;
				done = true;
				break;
			}

			currentIndex--;

			if (currentIndex < 0) {
				done = true;
				eof = true;
			}
		}

		return !eof;
	}

}
